/**
 * Copyright 2023 Huawei Cloud Computing Technologies Co., Ltd.

 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { EVENT, NamespacedLogger } from '@deepstream/types';
import { existsSync, promises } from 'fs';
import {
    relative,
    resolve,
    sep,
} from 'path';
import * as chokidar from 'chokidar';

const { readdir } = promises;

/**
 * Sync directory change observer
 */
export class SyncDirObserver {
    private watcher: chokidar.FSWatcher | null = null;
    private listenerQueue: Array<(fileList: string[]) => void> = [];
    private tempFileSet: Set<string> = new Set();
    private timer: NodeJS.Timeout | null = null;

    /**
     * @param syncDir
     * @param logger
     */
    constructor(private syncDir: string, private logger: NamespacedLogger) {
        const isExist = existsSync(this.syncDir);
        if (!isExist) {
            this.logger.fatal(EVENT.ERROR, 'Sync directory doesn\'t exist');
        }

        this.watchDir();
        this.listFiles().then((res) => {
            this.dispatch(res);
        });
    }

    /**
     * Add event listener
     * @param callback File add event listener
     */
    public onFileAdd(callback: (fileList: string[]) => void) {
        if (!this.listenerQueue.includes(callback)) {
            this.listenerQueue.push(callback);
        }
    }

    /**
     * Close watcher
     */
    public async close(): Promise<void> {
        await this.watcher?.close();
    }

    /**
     * Dispatch file add event
     * @param fileList
     */
    private dispatch(list: string[]) {
        this.listenerQueue.forEach((listener) => {
            listener(list);
        });
    }

    /**
     * Debounce dispatch, in case duplicate add/change events happen
     */
    private debounceDispatch() {
        if (this.timer) {
            clearTimeout(this.timer);
        }
        this.timer = setTimeout(() => {
            const list = Array.from(this.tempFileSet);
            // Clear temp files when dispatch
            this.tempFileSet = new Set();
            this.dispatch(list);
        }, 1000);
    }

    /**
     * Watch directory file change,
     * dispatch event when add/change
     */
    private watchDir() {
        this.watcher = chokidar.watch(this.syncDir, {
            ignoreInitial: true,
        })
            .on('add', (path: string) => {
                this.watchHandler(path);
            })
            .on('change', (path: string) => {
                this.watchHandler(path);
            });
    }

    private watchHandler(path: string) {
        let relativePath = relative(this.syncDir, path);
        // We need a file relative path separated by '/', to use in OBS service
        if (sep !== '/') {
            relativePath = relativePath.replace(new RegExp(`\\${sep}`, 'g'), '/');
        }
        this.tempFileSet.add(relativePath);
        this.debounceDispatch();
    }

    /**
     * List all files when init
     * @param relativePath
     * @returns
     */
    private async listFiles(relativePath = ''): Promise<string[]> {
        const dirPath = resolve(this.syncDir, relativePath);
        const direntList = await readdir(dirPath, { withFileTypes: true });
        let fileList: string[] = [];
        let tempList: string[] = [];
        for (let i = 0; i < direntList.length; i++) {
            const dirent = direntList[i];
            // We need a file relative path separated by  '/', to use in OBS service
            const subRelativePath = `${relativePath}${(relativePath && '/') || ''}${dirent.name}`;
            if (dirent.isDirectory()) {
                tempList = tempList.concat(await this.listFiles(subRelativePath));
            } else {
                fileList.push(subRelativePath);
            }
        }
        fileList = fileList.concat(tempList);
        return fileList;
    }
}
